Skip to content

fix(recording): stop the WGC helper from hanging on stop with no audio/webcam/cursor#121

Closed
EtienneLescot wants to merge 2 commits into
mainfrom
fix/wgc-stop-hang-no-audio-115
Closed

fix(recording): stop the WGC helper from hanging on stop with no audio/webcam/cursor#121
EtienneLescot wants to merge 2 commits into
mainfrom
fix/wgc-stop-hang-no-audio-115

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Fixes [Bug]: Windows native recorder never stops after stdin stop command (v1.7.0-rc.2) #115: on Windows, stopping a screen-only recording (system audio, mic, webcam, and cursor capture all disabled) never completed — the helper printed no [stop-timing] diagnostic at all and left a 0-byte MP4.
  • Root cause: the final "wait for stop" in wgc-capture's main() shared the same mutex/condition-variable as the video-writer thread's per-frame encode loop. That loop holds the mutex for the full duration of each frame's GPU copy + software encode call. If that pipeline stalls even briefly (slow software encoder, flaky GPU driver — the reporter's repro machine is a hybrid Intel UHD 620 / NVIDIA MX150 Optimus laptop), the main thread can't even acquire the lock to check whether stop was requested, turning a stall into a silent, unbounded hang. This surfaces specifically when audio/mic/webcam/cursor are disabled because nothing else exercises/releases that mutex on a faster, independent cadence.
  • Fix: CaptureControl now has a dedicated stopMutex/stopCv pair used only to signal "stop requested", fully decoupled from the frame-processing mutex. The stdin-reader thread notifies both the frame cv (unchanged, wakes the video-writer loop) and the new stop cv on every place that sets stopRequested. The final stop-wait now blocks only on the dedicated, uncontended pair, so stop detection is never gated on the frame pump releasing its lock.

How this was verified

Rebuilt wgc-capture.exe via CMake/Ninja from an x64 Native Tools prompt and drove it directly (no Electron) with a Node script that spawns the helper with the exact reported config (captureSystemAudio:false, captureMic:false, webcamEnabled:false, captureCursor:false, preferSoftwareEncoder:true), writes stop\n to stdin, and measures time-to-exit.

  • Before the fix could not be deterministically reproduced on this dev box (different Windows version/GPU than the reporter's), but the mutex-holding-across-a-GPU-call hazard is real and matches every symptom in the report (silent hang, zero [stop-timing] lines, 0-byte MP4).
  • After the fix: 20+ runs across 4s/20s/60s recording durations all stop within ~100-2200ms of stop, always emit the full [stop-timing] sequence, and produce non-zero-byte, valid MP4s.
  • Sanity check for the normal path (system audio + mic + cursor capture enabled) also stops cleanly (stopToExitMs ~780ms, full [stop-timing] log, valid MP4) — no regression.

Test plan

  • Rebuilt the native helper and reproduced the reported config directly (multiple runs, various durations)
  • Confirmed stop now always completes with a full [stop-timing] log and a valid, non-zero-byte MP4
  • Confirmed the full-featured path (system audio + mic + cursor) still stops correctly (no regression)
  • Manual smoke test on the real desktop app via npm run dev (native binary is gitignored; needs a maintainer/agent with Windows GUI access to click through record → stop → editor)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved capture shutdown responsiveness when stopping during intensive frame processing or encoding.
    • Ensured capture stops reliably after video or audio processing failures.
    • Prevented shutdown delays caused by contention with the frame-processing pipeline.

…o/webcam/cursor

The final "wait for stop" in wgc-capture's main() shared the same mutex/cv
as the video-writer thread's per-frame encode loop. That loop holds the
mutex for the full duration of each frame's GPU copy + software encode
call, so if the encode pipeline stalls (slow software encoder, flaky GPU
driver), the main thread can't even acquire the lock to check whether
`stop` was requested -- turning a stall into a silent, unbounded hang with
no [stop-timing] diagnostic output at all (issue #115). This reproduces
specifically when audio/mic/webcam/cursor are all disabled because in that
configuration nothing else is left holding/releasing that mutex on a
faster cadence to give the stop check a chance to run.

Give CaptureControl a dedicated stopMutex/stopCv used only to signal "stop
was requested", decoupled from the frame-processing mutex. The stdin
reader thread now notifies both cvs on stop, and the final stop-wait uses
only the dedicated pair, so stop detection never depends on the frame
pump releasing its lock.

Fixes #115
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@EtienneLescot, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6faba882-0893-4f2f-99bf-0ca9739e0afb

📥 Commits

Reviewing files that changed from the base of the PR and between 190fdc9 and 5ad5db0.

📒 Files selected for processing (1)
  • electron/native/wgc-capture/src/main.cpp
📝 Walkthrough

Walkthrough

The capture control now has dedicated stop synchronization. Command handling and frame, video, and audio failure paths notify stopCv, while the main shutdown flow waits on stopMutex and stopCv.

Changes

Capture shutdown signaling

Layer / File(s) Summary
Stop signal producers
electron/native/wgc-capture/src/main.cpp
Adds stopMutex and stopCv; stop commands and capture or encoding failures signal stopCv after setting stopRequested.
Dedicated shutdown wait
electron/native/wgc-capture/src/main.cpp
The main thread checks stopRequested while waiting on the dedicated stop synchronization pair.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

  • getopenscreen/openscreen issue 115 — Addresses the Windows native recorder hang by separating stop signaling from frame-processing synchronization.

Possibly related PRs

Suggested reviewers: my-denia

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: preventing WGC stop hangs for screen-only recordings.
Description check ✅ Passed The description covers the summary, issue reference, and testing well, and is mostly complete despite some optional template sections being omitted.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/wgc-stop-hang-no-audio-115

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
electron/native/wgc-capture/src/main.cpp (1)

627-631: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Use wait_for to prevent permanent hangs from lost wakeups on the video writer thread.

Because the stdin thread and audio callbacks cannot safely acquire the frame-processing mutex when signaling a stop (doing so would hang them if the encoder is stalled, defeating the very purpose of this PR), a lost wakeup can occur on cv. If the video writer thread is waiting on cv (e.g., when paused) and "stop" is received without holding mutex, the notification could be missed.

Unlike the main thread's initial startup wait which uses wait_for(10s) (L832), this wait has no timeout. A missed notification here will cause the video writer to hang indefinitely, which in turn hangs videoWriterThread.join() and the shutdown sequence.

Change wait to wait_for with a short timeout to ensure the thread always wakes up to check the stop condition even if a notification is missed. As per coding guidelines, Windows WGC native capture code is platform-fragile and must be treated as security-sensitive recorder code, requiring robust synchronization to prevent hangs.

💻 Proposed fix
-                control.cv.wait(lock, [&] {
+                control.cv.wait_for(lock, std::chrono::milliseconds(100), [&] {
                     return control.stopRequested.load() ||
                         encodeFailed.load() ||
                         (!control.paused.load() && latestFrameTexture);
                 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/wgc-capture/src/main.cpp` around lines 627 - 631, Update the
condition-variable wait in the video writer loop to use wait_for with a short
timeout, while retaining the existing stop, encode-failure, and
frame-availability predicate. Ensure periodic wakeups recheck these conditions
so a missed notification cannot leave videoWriterThread blocked indefinitely
during shutdown.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@electron/native/wgc-capture/src/main.cpp`:
- Around line 373-375: Protect every stopRequested update that wakes the main
thread with control.stopMutex to prevent lost wakeups. In
electron/native/wgc-capture/src/main.cpp, update ranges 373-375 and 393-395 with
scoped locks around the assignment, ranges 601-604, 673-676, and 686-689 with
scoped locks covering encodeFailed and stopRequested, and range 729-732 with a
scoped lock covering stopRequested; preserve the existing notifications.

---

Outside diff comments:
In `@electron/native/wgc-capture/src/main.cpp`:
- Around line 627-631: Update the condition-variable wait in the video writer
loop to use wait_for with a short timeout, while retaining the existing stop,
encode-failure, and frame-availability predicate. Ensure periodic wakeups
recheck these conditions so a missed notification cannot leave videoWriterThread
blocked indefinitely during shutdown.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7a2ae795-2356-4c6e-9540-7636a3dc7d3f

📥 Commits

Reviewing files that changed from the base of the PR and between d5966ed and 190fdc9.

📒 Files selected for processing (1)
  • electron/native/wgc-capture/src/main.cpp

Comment on lines 373 to +375
control.stopRequested = true;
control.cv.notify_all();
control.stopCv.notify_all();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Lost wakeup on stopCv due to unsynchronized state modification.

The main thread evaluates stopRequested while holding stopMutex (L866-L869). However, multiple paths in the code set stopRequested to true and notify stopCv without acquiring stopMutex. If the state change and notification occur exactly after the main thread evaluates the predicate as false but before it enters the OS wait queue, the notification is lost, and the main thread will hang indefinitely.

To prevent this race condition, any modification to stopRequested intended to wake the main thread must be protected by stopMutex. As per coding guidelines, Windows WGC native capture code is platform-fragile and must be treated as security-sensitive recorder code, requiring robust synchronization to prevent hangs.

  • electron/native/wgc-capture/src/main.cpp#L373-L375: wrap the assignment to control.stopRequested in a std::scoped_lock lock(control.stopMutex);.
  • electron/native/wgc-capture/src/main.cpp#L393-L395: wrap the assignment to control.stopRequested in a std::scoped_lock lock(control.stopMutex);.
  • electron/native/wgc-capture/src/main.cpp#L601-L604: wrap the assignments to encodeFailed and control.stopRequested in a std::scoped_lock stopLock(control.stopMutex);.
  • electron/native/wgc-capture/src/main.cpp#L673-L676: wrap the assignments to encodeFailed and control.stopRequested in a std::scoped_lock stopLock(control.stopMutex);.
  • electron/native/wgc-capture/src/main.cpp#L686-L689: wrap the assignments to encodeFailed and control.stopRequested in a std::scoped_lock stopLock(control.stopMutex);.
  • electron/native/wgc-capture/src/main.cpp#L729-L732: wrap the assignments to encodeFailed and control.stopRequested in a std::scoped_lock lock(control.stopMutex);.
📍 Affects 1 file
  • electron/native/wgc-capture/src/main.cpp#L373-L375 (this comment)
  • electron/native/wgc-capture/src/main.cpp#L393-L395
  • electron/native/wgc-capture/src/main.cpp#L601-L604
  • electron/native/wgc-capture/src/main.cpp#L673-L676
  • electron/native/wgc-capture/src/main.cpp#L686-L689
  • electron/native/wgc-capture/src/main.cpp#L729-L732
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/wgc-capture/src/main.cpp` around lines 373 - 375, Protect
every stopRequested update that wakes the main thread with control.stopMutex to
prevent lost wakeups. In electron/native/wgc-capture/src/main.cpp, update ranges
373-375 and 393-395 with scoped locks around the assignment, ranges 601-604,
673-676, and 686-689 with scoped locks covering encodeFailed and stopRequested,
and range 729-732 with a scoped lock covering stopRequested; preserve the
existing notifications.

Source: Coding guidelines

… hang

Per CodeRabbit review on #121: the video-writer thread's frame-wait held an
unbounded cv.wait() on the same predicate this PR already fixed for the
stop-path. Switch it to wait_for(100ms) so the loop always re-checks
stopRequested/encodeFailed even in the (currently unreachable, but
defense-in-depth) case of a missed notification, instead of relying solely on
notify_all always reaching an already-waiting thread.

Verified: rebuilt wgc-capture.exe, re-ran the exact #115 repro (screen-only,
all extras disabled) and the full-featured regression (system audio + cursor)
- both stop promptly with a full [stop-timing] log and a valid non-zero MP4.
EtienneLescot added a commit that referenced this pull request Jul 19, 2026
Follow-up to #119 (Fixes #115). CodeRabbit flagged, on the now-superseded
#121, that the video-writer thread's per-frame cv.wait() has no timeout: if a
notification were ever missed, the thread would block indefinitely, hanging
videoWriterThread.join() and the whole stop sequence — exactly the failure
mode this release is eliminating. #119 already fixed the actual root cause
(the blocking WriteSample call no longer runs under this mutex, so the main
thread's stop-wait is never starved), but the wait itself was still
unbounded. Switch it to wait_for(100ms) so the loop always re-checks
stopRequested/encodeFailed even in that circumstance, instead of relying
solely on a notification reaching an already-waiting thread.

Verified: rebuilt wgc-capture.exe on top of current main (includes #119),
re-ran the #115 repro (screen-only, all extras disabled) and the
full-featured regression (system audio + cursor) - both stop in under 110ms
with a full [stop-timing] log and a valid MP4.
@EtienneLescot
EtienneLescot deleted the fix/wgc-stop-hang-no-audio-115 branch July 19, 2026 09:29
EtienneLescot added a commit that referenced this pull request Jul 19, 2026
Follow-up to #119 (Fixes #115). CodeRabbit flagged, on the now-superseded
#121, that the video-writer thread's per-frame cv.wait() has no timeout: if a
notification were ever missed, the thread would block indefinitely, hanging
videoWriterThread.join() and the whole stop sequence — exactly the failure
mode this release is eliminating. #119 already fixed the actual root cause
(the blocking WriteSample call no longer runs under this mutex, so the main
thread's stop-wait is never starved), but the wait itself was still
unbounded. Switch it to wait_for(100ms) so the loop always re-checks
stopRequested/encodeFailed even in that circumstance, instead of relying
solely on a notification reaching an already-waiting thread.

Verified: rebuilt wgc-capture.exe on top of current main (includes #119),
re-ran the #115 repro (screen-only, all extras disabled) and the
full-featured regression (system audio + cursor) - both stop in under 110ms
with a full [stop-timing] log and a valid MP4.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Windows native recorder never stops after stdin stop command (v1.7.0-rc.2)

1 participant